package org.oos.dao.impl;

import static com.google.api.client.http.HttpMethod.DELETE;
import static com.google.api.client.http.HttpMethod.GET;
import static com.google.api.client.http.HttpMethod.POST;
import static com.google.api.client.http.HttpMethod.PUT;
import static com.google.api.client.http.xml.atom.AtomContent.forFeed;
import static com.google.api.client.http.xml.atom.AtomContent.forEntry;
import static org.oos.Constants.Dictionary.Atom;
import static org.oos.Constants.Dictionary.Xml;
import static org.oos.atom.AtomResponse.ResponseType.Empty;
import static org.oos.atom.AtomService.atomService;
import static org.oos.dao.options.OptionsMap.FINDERS.PAGE_OPTIONS;
import static org.oos.dao.options.OptionsMap.FINDERS.QUERY_OPTIONS;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

import org.oos.atom.AtomEntity;
import org.oos.atom.AtomRequest;
import org.oos.atom.AtomRequest.Builder;
import org.oos.atom.AtomResponse;
import org.oos.atom.AtomService.HttpStatusException;
import org.oos.atom.impl.AtomFeed;
import org.oos.auth.AuthenticationHandler;
import org.oos.dao.Dao;
import org.oos.dao.SiteDao;
import org.oos.dao.exceptions.CreationException;
import org.oos.dao.exceptions.DeleteException;
import org.oos.dao.exceptions.ReadException;
import org.oos.dao.exceptions.UpdateException;
import org.oos.dao.options.OptionsMap;
import org.oos.dao.options.PageOptions;
import org.oos.lists.DynamicList;
import org.oos.logging.ConsoleLogger;

import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpParser;
import com.google.api.client.http.InputStreamContent;
import com.google.api.client.http.UrlEncodedContent;
import com.google.api.client.http.xml.XmlHttpParser;
import com.google.api.client.http.xml.atom.AtomContent;
import com.google.api.client.http.xml.atom.AtomParser;
import com.google.api.client.xml.XmlNamespaceDictionary;

/**
 * This abstract implementation of {@link Dao} provides the basic independent
 * processing that is extended by subclasses. A sub-class must provide with:
 * <ul>
 * <li>
 * The Class type for the feed that will be processed on read: &lt;Feed&gt;</li>
 * <li>
 * The URL to get the feed: {@link AbstractDao#getFeedUrl()}</li>
 * <li>
 * The URL to interact with an entry: 
 * <ul> <li>
 * {@link AbstractDao#getEntryUrl(AtomEntity)} (for updating entries) and/or</li>
 * <li> {@link AbstractDao#getEntryUrl(String)} (for reading entries) </li></ul></li>
 * <li>
 * A method to convert a Feed into a DynamicList: {@link AbstractDao#getList(AtomFeed)}</li>
 * <li>
 * Whether the DAO requires authorization or not</li>
 * </ul>
 * 
 * @author pablo.gonzalez@11870.com
 * @see Dao
 * @see AtomEntity
 * @see AtomFeed
 * @see SiteDao
 * 
 * @param <Entry> extends {@link AtomEntity}
 * @param <Feed> extends {@link AtomFeed} of &lt;Entry&gt; values
 * 
 */
public abstract class AbstractDao<Entry extends AtomEntity, Feed extends AtomFeed<Entry>>
		implements Dao<Entry, Feed> {

	private static final long serialVersionUID = 1L;
	private static String uuid = null;

	protected Logger log = Logger.getLogger("DAO");
	private Feed feed;
	private DynamicList<Entry> list;
	private List<OptionsMap> options;
	protected AuthenticationHandler authHandler;

	public Class<Feed> feedType;
	private Class<Entry> entryType;

	private HttpParser parser;

	@SuppressWarnings("unchecked")
	AbstractDao(Class<?> feedType, Class<?> entryType) {
		try {
			this.feedType = (Class<Feed>) feedType;
			this.entryType = (Class<Entry>) entryType;
		} catch (ClassCastException e) {
			throw new IllegalArgumentException(
					"This Dao must provide with a valid AtomFeed/AtomEntity "
							+ "as the arguments of the super constructors");
		}
	}

	@Override
	public Dao<Entry, Feed> setAuthenticationHandler(
			AuthenticationHandler authHandler) {
		this.authHandler = authHandler;
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.oos.dao.Dao#setLog(java.util.logging.Logger)
	 */
	@Override
	public Dao<Entry, Feed> setLog(Logger log) {
		this.log = log;
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.oos.dao.Dao#getLog()
	 */
	@Override
	public Logger getLog() {
		if (log == null) {
			log = new ConsoleLogger();
		}
		return log;
	}

	public HttpParser getParser() {
		if (parser == null) {
			if (getContentType() != null) {
				parser = XmlHttpParser
						.builder(getPlainXmlNamespaceDictionary())
						.setContentType(getContentType()).build();
			} else {
				parser = new AtomParser(getXmlNamespaceDictionary());
			}
		}
		return parser;
	}

	/**
	 * Override this method to provide a different content type.
	 * 
	 * @return
	 */
	protected String getContentType() {
		return null;
	}

	/**
	 * Override this method to provide with a different
	 * {@link XmlNamespaceDictionary}
	 * 
	 * @return
	 */
	protected XmlNamespaceDictionary getXmlNamespaceDictionary() {
		return Atom.getDictionary();
	}

	protected XmlNamespaceDictionary getPlainXmlNamespaceDictionary() {
		return Xml.getDictionary();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.oos.dao.Dao#read()
	 */
	@SuppressWarnings("unchecked")
	@Override
	public <L extends DynamicList<Entry>> L read() throws ReadException {
		if (feed == null) {
			feed = readFeed(1);
			list = getList(feed);
		}
		return (L) list;
	}

	public static void setUuid(String uuid) {
		AbstractDao.uuid = uuid;
	}

	public static String getUuid() {
		return uuid;
	}

	protected Builder requestBuilder() {
		return AtomRequest.requestBuilder() //
				.setAuthHandler(authHandler) //
				.setParameters(getOptions()) //
				.setHttpParser(getParser()); //
	}

	@Override
	public Feed readFeed(Integer index) throws ReadException {
		PageOptions pageOptions = PAGE_OPTIONS.get(getOptions());
		pageOptions.setPage(index);
		if (getUuid() != null) {
			QUERY_OPTIONS.get(getOptions()).setUuid(getUuid());
		}
		AtomRequest request = requestBuilder() //
				.setUrl(getFeedUrl()) //
				.setHttpMethod(GET) //
				.build(); //
		AtomResponse<Feed> response = atomService().execute(request,
				getFeedType());
		if (response.getResult() != null) {
			return response.getResult();
		} else {
			throw new ReadException(response.getThrowable());
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.oos.dao.Dao#read(java.lang.Integer)
	 */
	@Override
	public Entry read(Integer index) throws ReadException {
		return read().get(index);
	}

	@Override
	public Entry readEntry(String param) throws ReadException {
		// try {
		AtomRequest request = requestBuilder() //
				.setUrl(getEntryUrl(param)) //
				.setHttpMethod(GET) //
				.build();
		AtomResponse<Entry> response = atomService().execute(request,
				getEntryType());
		switch (response.getType()) {
		case Valid:
			return response.getResult();
		case Error:
			throw new ReadException(response.getThrowable());
		case Empty:
			throw new ReadException("No body returned with request");
		case Unknown:
		default:
			return null;
		}

		// Entry entry = AtomService.executeGet(authHandler,
		// createTransport(), getEntryUrl(param), getOptions(),
		// getEntryType());
		// return entry;
		// } catch (HttpStatusException e) {
		// if (e.getStatusCode() == 404) {
		// return null;
		// }
		// throw new ReadException(e);
		// } catch (Exception e) {
		// throw new ReadException(e);
		// }
	}

	@Override
	public InputStream readMedia(String param) throws ReadException {
		AtomRequest request = requestBuilder() //
				.setUrl(getMediaUrl(param)) //
				.setHttpMethod(GET) //
				.build(); //

		AtomResponse<Entry> response = atomService().execute(request);
		try {
			if (response.getType() == Empty) {
				return response.getHttpResponse().getContent();
			} else {
				throw response.getThrowable();
			}
		} catch (Throwable e) {
			throw new ReadException(e);
		}
		//
		// try {
		// HttpResponse response = AtomService.executeRawGet(authHandler,
		// createTransport(), getMediaUrl(param), getOptions());
		// return response.getContent();
		// } catch (Exception e) {
		// throw new ReadException(e);
		// }
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.oos.dao.Dao#create(org.oos.atom.AtomEntity)
	 */
	@Override
	public Entry create(Entry entry) throws CreationException {

		AtomContent content = forEntry(getXmlNamespaceDictionary(), entry);
		AtomRequest request = requestBuilder() //
				.setUrl(getFeedUrl()) //
				.setContent(content) //
				.setHttpMethod(POST) //
				.build(); //
		AtomResponse<Entry> response;
		response = atomService().execute(request, getEntryType());

		switch (response.getType()) {
		case Valid:
			return response.getResult();
		case Error:
			throw new CreationException(response.getThrowable());
		case Unknown:
		case Empty:
		default:
			return null;
		}
	}

	@Override
	public Entry create() throws CreationException {
		try {
			UrlEncodedContent content = new UrlEncodedContent(null);
			AtomRequest request = requestBuilder() //
					.setUrl(getFeedUrl()) //
					.setContent(content) //
					.setHttpMethod(POST) //
					.build(); //
			AtomResponse<Entry> response;
			response = atomService().execute(request, getEntryType());
			switch (response.getType()) {
			case Valid:
				return response.getResult();
			case Error:
				throw response.getThrowable();
			case Empty:
				throw new IllegalStateException(
						"No body returned when creating");
			case Unknown:
			default:
				throw new IllegalArgumentException("Unknown error");
			}
		} catch (Throwable e) {
			CreationException exception;
			if (e instanceof HttpStatusException) {
				exception = new CreationException(e.getMessage());
				exception.setResponse(((HttpStatusException) e).getResponse());
			} else {
				exception = new CreationException(e);
			}
			throw exception;
		}
	}

	@Override
	public Entry createMedia(File file, String contentType)
			throws CreationException {
		try {
			InputStreamContent content = new InputStreamContent(contentType,
					new FileInputStream(file));
			content.setLength(file.length());
			AtomRequest request = requestBuilder() //
					.setContent(content)
					.setHttpMethod(POST)//
					.setUrl(getFeedUrl()) //
					.build();
			AtomResponse<Entry> response = atomService().execute(request,
					getEntryType());
			switch (response.getType()) {
			case Valid:
				return response.getResult();
			case Error:
				throw response.getThrowable();
			default:
				throw new IllegalArgumentException();
			}
		} catch (Throwable e) {
			throw new CreationException(e);
		}

		// try {
		// InputStreamContent content = new InputStreamContent();
		// content.inputStream = new FileInputStream(file);
		// content.type = contentType;
		// content.length = file.length();
		// return AtomService.executePost(authHandler, createTransport(),
		// getFeedUrl(), content, getOptions(), getEntryType());
		// } catch (Exception e) {
		// throw new CreationException(e);
		// }
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.oos.dao.Dao#delete(org.oos.atom.AtomEntity)
	 */
	@Override
	public boolean delete(Entry entry) throws DeleteException {
		AtomRequest request = requestBuilder() //
				.setUrl(getFeedUrl()) //
				.setHttpMethod(DELETE) //
				.build();
		AtomResponse<Entry> response = atomService().execute(request);
		try {
			switch (response.getType()) {
			case Valid:
			case Empty:
				return true;
			case Error:
				throw response.getThrowable();
			case Unknown:
			default:
				throw new IllegalStateException("Uknown error");
			}
		} catch (Throwable e) {
			throw new DeleteException(e);
		}
		// try {
		// return AtomService.executeDelete(authHandler, createTransport(),
		// getEntryUrl(entry));
		// } catch (Exception e) {
		// throw new DeleteException(e);
		// }
	}

	@Override
	public boolean deleteMedia(Entry entry) throws DeleteException {
		AtomRequest request = requestBuilder() //
				.setUrl(getMediaUrl(entry)) //
				.setHttpMethod(DELETE) //
				.build();
		AtomResponse<Entry> response = atomService().execute(request);
		try {
			switch (response.getType()) {
			case Valid:
			case Empty:
				return true;
			case Error:
				throw response.getThrowable();
			case Unknown:
			default:
				throw new IllegalStateException("Uknown error");
			}
		} catch (Throwable e) {
			throw new DeleteException(e);
		}
		//
		// try {
		// return AtomService.executeDelete(authHandler, createTransport(),
		// getMediaUrl(entry));
		// } catch (Exception e) {
		// throw new DeleteException(e);
		// }
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.oos.dao.Dao#update(org.oos.atom.AtomEntity)
	 */
	@Override
	public boolean update(Entry entry) throws UpdateException {
		AtomContent content = AtomContent.forEntry(getXmlNamespaceDictionary(),
				entry);
		AtomRequest request = requestBuilder() //
				.setUrl(getEntryUrl(entry)) //
				.setHttpMethod(PUT) //
				.setContent(content) //
				.build();
		AtomResponse<Entry> response = atomService().execute(request);
		try {
			switch (response.getType()) {
			case Valid:
			case Empty:
				return true;
			case Error:
				throw response.getThrowable();
			case Unknown:
			default:
				throw new IllegalStateException("Uknown error");
			}
		} catch (Throwable e) {
			throw new UpdateException(e);
		}
		// try {
		// AtomContent content = new AtomContent();
		// content.entry = entry;
		// content.namespaceDictionary = getXmlNamespaceDictionary();
		// return AtomService.executePut(authHandler, createTransport(),
		// getEntryUrl(entry), content);
		// } catch (Exception e) {
		// throw new UpdateException(e);
		// }
	}

	@Override
	public boolean updateMedia(Entry entry, File file, String contentType)
			throws UpdateException {
		try {
			InputStreamContent content = new InputStreamContent(contentType,
					new FileInputStream(file));
			content.setLength(file.length());
			AtomRequest request = requestBuilder() //
					.setUrl(getMediaUrl(entry)) //
					.setContent(content) //
					.build();
			AtomResponse<Entry> response = atomService().execute(request);
			switch (response.getType()) {
			case Valid:
			case Empty:
				return true;
			case Error:
				throw response.getThrowable();
			case Unknown:
			default:
				throw new IllegalStateException("Unknown error");
			}
		} catch (Throwable e) {
			throw new UpdateException(e);
		}

		// try {
		// InputStreamContent content = new InputStreamContent();
		// content.inputStream = new FileInputStream(file);
		// content.type = contentType;
		// content.length = file.length();
		// return AtomService.executePut(authHandler, createTransport(),
		// getMediaUrl(entry), content);
		// } catch (Exception e) {
		// throw new UpdateException(e);
		// }
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.oos.dao.Dao#size()
	 */
	@Override
	public int size() throws ReadException {
		return read().size();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.oos.dao.Dao#getOptions()
	 */
	@Override
	public List<OptionsMap> getOptions() {
		if (options == null) {
			options = new ArrayList<OptionsMap>();
		}
		return options;
	}

	/* Method to be implemented by concrete DAOs */

	/**
	 * It should return a {@link Class} of the type that is processed by the DAO
	 */
	@SuppressWarnings("unchecked")
	<F extends Feed> Class<F> getFeedType() {
		if (feedType == null) {
			throw new IllegalStateException("The Feed type must be set.");
		}
		return (Class<F>) feedType;
	}

	@SuppressWarnings("unchecked")
	<E extends Entry> Class<E> getEntryType() {
		if (feedType == null) {
			throw new IllegalStateException("The Entry type must be set.");
		}
		return (Class<E>) entryType;
	}

	/**
	 * @return The {@link OOSUrl} for interacting with the feed.
	 */
	protected abstract GenericUrl getFeedUrl();

	/**
	 * @return The {@link OOSUrl} for the interaction with an entry
	 */
	protected GenericUrl getEntryUrl(Entry entry) {
		throw new UnsupportedOperationException();
	}
 
	protected GenericUrl getEntryUrl(String param) {
		throw new UnsupportedOperationException();
	}

	protected GenericUrl getMediaUrl(Entry entry) {
		throw new UnsupportedOperationException();
	}

	protected GenericUrl getMediaUrl(String param) {
		throw new UnsupportedOperationException();
	}

	/**
	 * Converts the provided Feed to a {@link DynamicList}
	 * 
	 * @param <L>
	 * @param feed
	 * @return
	 */
	protected abstract <L extends DynamicList<Entry>> L getList(Feed feed);

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return getFeedUrl().toString();
	}

}
